﻿using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Hyperion.Native;

namespace Hyperion
{
    /// <summary>
    /// Base class for all memory manipulation classes, in-process or external.
    /// </summary>
    public abstract class Memory : IDisposable
    {
        public delegate void ExitDelegate(object sender, EventArgs args);
        /// <summary>
        /// Occurs when [on process exited].
        /// </summary>
        public event ExitDelegate OnProcessExited;

        protected virtual void InvokeOnProcessExited(object sender, EventArgs args)
        {
            ExitDelegate handler = OnProcessExited;
            if (handler != null) handler(this, args);
        }

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="Memory"/> class.
        /// </summary>
        /// <param name="proc">The proc.</param>
        protected Memory(Process proc)
        {
            proc.EnableRaisingEvents = true;

            // Handle the process exiting properly, and throw an event to anyone interested.
            proc.Exited += (sender, args) =>
                               {
                                   InvokeOnProcessExited(sender, args);
                                   Dispose();
                               };

            // Whenever we receive Error or Output data from the process, write it to the debug.
            proc.ErrorDataReceived += (sender, args) => Debug.Write(args.Data);
            proc.OutputDataReceived += (sender, args) => Debug.Write(args.Data);

            Process = proc;

            ProcessHandle =
                Win32.OpenProcess(
                    ProcessAccessFlags.QueryInformation | ProcessAccessFlags.VMRead | ProcessAccessFlags.VMWrite |
                    ProcessAccessFlags.VMOperation | ProcessAccessFlags.CreateThread, false, proc.Id);

            if (ProcessHandle.IsInvalid)
                throw new MemoryException("ProcessHandle was invalid! Make sure we have the required privileges to call OpenProcess with the desired flags! Last Win32 error: " + Marshal.GetLastWin32Error());

            Process.EnterDebugMode();
        }

        #endregion

        /// <summary>
        /// The process we're operating on.
        /// </summary>
        public Process Process { get; private set; }

        /// <summary>
        /// Obtains a safe ProcessHandle to the process we're attached to.
        /// </summary>
        public SafeMemoryHandle ProcessHandle { get; private set; }

        private IntPtr Handle { get { return ProcessHandle.DangerousGetHandle(); } }

        /// <summary>
        /// Gets the base address of the process' MainModule.
        /// </summary>
        public IntPtr BaseAddress
        {
            get { return Process != null && !Process.HasExited ? Process.MainModule.BaseAddress : IntPtr.Zero; }
        }

        /// <summary>
        /// Gets the relative address from the provided absolute one.
        /// </summary>
        /// <param name="absolute">The absolute.</param>
        /// <returns></returns>
        public IntPtr GetRelative(IntPtr absolute)
        {
            return new IntPtr(absolute.ToInt64() - BaseAddress.ToInt64());
        }

        /// <summary>
        /// Gets the absolute address from the provided relative one.
        /// </summary>
        /// <param name="relative">The relative.</param>
        /// <returns></returns>
        public IntPtr GetAbsolute(IntPtr relative)
        {
            return new IntPtr(relative.ToInt64() + BaseAddress.ToInt64());
        }

        #region Read/Write

        /// <summary>
        /// Reads the specified amount of bytes from the specified memory location.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="count">The count.</param>
        /// <param name="isRelative">if set to <c>true</c> [is relative].</param>
        /// <returns></returns>
        public byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
        {
            var ret = new byte[count];
            int numRead;

            if (isRelative)
                address = new IntPtr(address.ToInt64() + BaseAddress.ToInt64());

            // Ensure we've read the amount we should have while we're at it. Kthx.
            if (Win32.ReadProcessMemory(Handle, address, ret, count, out numRead) && numRead == count)
                return ret;

            throw new MemoryException("ReadBytes failed! Only read {0} bytes, while {1} were expected!", numRead, count);
        }

        /// <summary>
        /// Writes the specified bytes to the specified address.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="bytes">The bytes.</param>
        /// <param name="isRelative">if set to <c>true</c> [is relative].</param>
        /// <returns></returns>
        public int WriteBytes(IntPtr address, byte[] bytes, bool isRelative = false)
        {
            int numWritten;

            if (Win32.WriteProcessMemory(Handle, address, bytes, (uint)bytes.Length, out numWritten) && numWritten == bytes.Length)
                return numWritten;

            throw new MemoryException("WriteBytes failed! {0} bytes written; {1} expected!", numWritten, bytes.Length);
        }

        /// <summary>
        /// Reads the specified address.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="address">The address.</param>
        /// <param name="isRelative">if set to <c>true</c> [is relative].</param>
        /// <returns></returns>
        public abstract T Read<T>(IntPtr address, bool isRelative = false) where T : struct;

        /// <summary>
        /// Reads the specified addresses.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="addresses">The addresses.</param>
        /// <param name="isRelative">if set to <c>true</c> [is relative].</param>
        /// <returns></returns>
        public abstract T Read<T>(bool isRelative = false, params IntPtr[] addresses) where T : struct;

        /// <summary>
        /// Writes to the specified address.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="address">The address.</param>
        /// <param name="value">The value.</param>
        /// <param name="isRelative">if set to <c>true</c> [is relative].</param>
        public abstract int Write<T>(IntPtr address, T value, bool isRelative = false) where T : struct;

        #endregion

        #region Strings

        /// <summary>
        /// Reads a string from the specified address.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="encoding">The encoding.</param>
        /// <param name="maxLength">Length of the max.</param>
        /// <param name="relative">if set to <c>true</c> [relative].</param>
        /// <returns></returns>
        public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
        {
            byte[] buffer = ReadBytes(address, maxLength, relative);
            string ret = encoding.GetString(buffer);

            if (ret.IndexOf('\0') != -1)
                ret = ret.Remove(ret.IndexOf('\0'));
            return ret;
        }

        /// <summary>
        /// Writes a string to the specified address
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="value">The value.</param>
        /// <param name="encoding">The encoding.</param>
        /// <param name="relative">if set to <c>true</c> [relative].</param>
        /// <returns></returns>
        public virtual bool WriteString(IntPtr address, string value, Encoding encoding, bool relative = false)
        {
            if (value[value.Length - 1] != '\0')
                value += '\0';

            byte[] b = encoding.GetBytes(value);
            int written = WriteBytes(address, b, relative);
            return written == b.Length;
        }

        #endregion

        #region Implementation of IDisposable

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Process.LeaveDebugMode();
        }

        #endregion
    }

    #region Type : MemoryException

    /// <summary>
    /// Exception that occurs within Hyperion itself. Hyperion will exclusively throw this type of exception throughout its lifespan.
    /// </summary>
    public class MemoryException : Exception
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MemoryException"/> class.
        /// </summary>
        /// <param name="message">The message.</param>
        public MemoryException(string message)
            : base(message)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MemoryException"/> class.
        /// </summary>
        /// <param name="format">The format.</param>
        /// <param name="args">The args.</param>
        public MemoryException(string format, params object[] args)
            : base(string.Format(format, args))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MemoryException"/> class with an inner exception.
        /// </summary>
        /// <param name="format">The format.</param>
        /// <param name="inner">The inner.</param>
        /// <param name="args">The args.</param>
        public MemoryException(string format, Exception inner, params object[] args)
            : base(string.Format(format, args), inner)
        {
        }
    }

    #endregion
}
